package magnet.processor.instances.aspects.type;

import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import magnet.Instance;
import magnet.Scoping;
import magnet.processor.MagnetProcessorEnv;
import magnet.processor.common.CompilationException;
import magnet.processor.common.ValidationException;
import magnet.processor.instances.parser.AspectValidator;
import magnet.processor.instances.parser.ParserInstance;

import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class TypeAndTypesValidator implements AspectValidator {

    private static volatile TypeAndTypesValidator typeAndTypesValidator;

    public static TypeAndTypesValidator INSTANCE() {
        synchronized (TypeAndTypesValidator.class) {
            if (typeAndTypesValidator == null) {
                synchronized (TypeAndTypesValidator.class) {
                    typeAndTypesValidator = new TypeAndTypesValidator();
                }
            }
        }
        return typeAndTypesValidator;
    }

    @Override
    public <E extends Element> ParserInstance<E> validate(ParserInstance<E> parserInstance, MagnetProcessorEnv env) throws ValidationException, CompilationException {
        E element = parserInstance.getElement();
        TypeElement declaredType = parserInstance.getDeclaredType();
        List<TypeElement> declaredTypes = parserInstance.getDeclaredTypes();
        ParserInstance<E> elementWithType = parserInstance;
        if (element instanceof TypeElement && declaredType == null && (declaredTypes == null || declaredTypes.isEmpty())) {
            elementWithType = parserInstance.copy();
            elementWithType.setDeclaredType(autodetectType((TypeElement) element, env));
        }

        return validateTypes(elementWithType);
    }

    private TypeElement autodetectType(TypeElement typeElement, MagnetProcessorEnv env) throws ValidationException {
        if (typeElement.getInterfaces().isEmpty()) {
            TypeName superType = ClassName.get(typeElement.getSuperclass());
            if (superType instanceof ClassName && ((ClassName) superType).reflectionName().equals("java.lang.Object")) {
                return typeElement;
            }
        }
        throw new ValidationException(typeElement,
                Instance.class + " must declare either 'type' or 'types' property.");
    }

    private <E extends Element> ParserInstance<E> validateTypes(ParserInstance<E> parserInstance) throws ValidationException, CompilationException {
        TypeElement declaredType = parserInstance.getDeclaredType();
        List<TypeElement> declaredTypes = parserInstance.getDeclaredTypes();
        boolean isTypeDeclared = declaredType != null;
        boolean areTypesDeclared = false;
        if (declaredTypes != null && !declaredTypes.isEmpty()) {
            areTypesDeclared = true;
        }
        if (isTypeDeclared && areTypesDeclared) {
            throw new ValidationException(parserInstance.getElement(),
                    Instance.class + " must declare either 'type' or 'types' property, not both.");
        }

        if (declaredType != null) {
            List<TypeElement> types = Arrays.asList(declaredType);
            ParserInstance instance = parserInstance.copy();
            instance.setDeclaredTypes(Arrays.asList(declaredType));
            List<ClassName> classNames = new ArrayList<>();
            for (TypeElement item : types) {
                classNames.add(ClassName.get(item));
            }
            instance.setTypes(classNames);
            return instance;
        }

        if (declaredTypes != null) {
            if (parserInstance.getScoping().equals(Scoping.UNSCOPED.name())) {
                throw new ValidationException(parserInstance.getElement(),
                        "types() property must be used with scoped instances only. Set " +
                                "scoping to Scoping.DIRECT or Scoping.TOPMOST.");
            }
            ParserInstance instance = parserInstance.copy();
            List<ClassName> classNames = new ArrayList<>();
            for (TypeElement item : declaredTypes) {
                classNames.add(ClassName.get(item));
            }
            instance.setTypes(classNames);
            return instance;
        }

        throw new CompilationException(parserInstance.getElement(),
                "Cannot verify type declaration.");
    }
}
